package org.putrpctorest.service.impl.copy;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtConstructor;
import javassist.CtMethod;
import javassist.NotFoundException;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.Descriptor;
import javassist.bytecode.MethodInfo;
import javassist.bytecode.SignatureAttribute;
import javassist.bytecode.annotation.ArrayMemberValue;
import javassist.bytecode.annotation.MemberValue;
import javassist.bytecode.annotation.StringMemberValue;

import javax.ws.rs.Consumes;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;

import org.putrpctorest.PutRpcToRestError;
import org.putrpctorest.container.Container;
import org.putrpctorest.resource.Method;
import org.putrpctorest.resource.Parameter;
import org.putrpctorest.resource.Resource;

public class JavassistServiceClassGenerator {
    // implements ServiceClassGenerator {

    private List<String> methodNames = new ArrayList<String>();
    private ClassPool pool = ClassPool.getDefault();
    private ConstPool constpool;
    private Class<?> generatedClass;

    public JavassistServiceClassGenerator() {
    }

    public Class<?> generateClassForModel(
    // Services restServices
    ) {
        // String className = getClassName(restServices);

        // CtClass classModel = pool.makeClass(className);

        // ClassFile ccFile = classModel.getClassFile();

        // ccFile.setVersionToJava5();
        //
        // constpool = ccFile.getConstPool();

        AnnotationsAttribute attribute = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
        javassist.bytecode.annotation.Annotation annotation = new javassist.bytecode.annotation.Annotation(
                Path.class.getName(), constpool);
        annotation.addMemberValue("value", new StringMemberValue("/", constpool));
        attribute.setAnnotation(annotation);

        // ccFile.addAttribute(attribute);

        // try {
        // CtField containerField = new CtField(getCtClassForClass(Container.class), "container", classModel);
        // classModel.addField(containerField);
        // } catch (Exception e) {
        // throw new PutRpcToRestError("Error creating service class. " + e, e);
        // }
        // generateConstructor(classModel);
        // for (Resource resource : restServices.getResources()) {
        // for (Method method : resource.getMethods()) {
        // generateMethod(classModel, resource, method);
        // }
        // }
        //
        // try {
        // return classModel.toClass();
        // } catch (Exception e) {
        // throw new PutRpcToRestError("Error creating service class. " + e, e);
        // }
        return null;
    }

    private void generateConstructor(CtClass restClass) {
        try {
            CtConstructor contructor = new CtConstructor(new CtClass[] {getCtClassForClass(Container.class)}, restClass);

            StringBuffer body = new StringBuffer();
            body.append("{");
            body.append("  this.container = $1;");
            body.append("}");
            contructor.setBody(body.toString());

            restClass.addConstructor(contructor);
        } catch (CannotCompileException e) {
            throw new PutRpcToRestError("Error creatin constructor for service class. " + e, e);
        }

    }

    private void generateMethod(CtClass restClass, Resource resource, Method method) {
        String methodName = getMethodName(resource, method);

        try {
            CtMethod createdMethod = new CtMethod(getCtClassForClass(Object.class), methodName, getMethodSignature(
                    resource, method), restClass);

            MethodInfo methodInfo = createdMethod.getMethodInfo();
            SignatureAttribute signatureAttribute = new SignatureAttribute(methodInfo.getConstPool(),
                    getJvmSignature(method));

            methodInfo.addAttribute(signatureAttribute);

            StringBuffer body = new StringBuffer();
            body.append("{");
            body.append("  java.util.Map params = new java.util.HashMap();");
            int i = 1;
            for (Parameter parameter : method.getParameters()) {
                body.append("  params.put(");
                body.append("\"");
                body.append(parameter.getName());
                body.append("\"");
                body.append(",");
                addManualBoxing(body, parameter, i);
                body.append(");");
                i++;
            }
            body.append("  String methodName = \"");
            // body.append(method.getName());
            body.append("\"");
            body.append("; return ");
            body.append(" container.getServiceInvoker().invokeService(params,methodName ); }");
            createdMethod.setBody(body.toString());

            AnnotationsAttribute attribute = new AnnotationsAttribute(constpool, AnnotationsAttribute.visibleTag);
            javassist.bytecode.annotation.Annotation annotation = new javassist.bytecode.annotation.Annotation(
                    Path.class.getName(), constpool);
            annotation.addMemberValue("value", new StringMemberValue(method.getResource().getPath(), constpool));
            attribute.addAnnotation(annotation);

            annotation = new javassist.bytecode.annotation.Annotation(Consumes.class.getName(), constpool);
            ArrayMemberValue arrayMemberValue = new ArrayMemberValue(constpool);
            arrayMemberValue.setValue(getConsumes(method));
            annotation.addMemberValue("value", arrayMemberValue);
            attribute.addAnnotation(annotation);

            annotation = new javassist.bytecode.annotation.Annotation(Produces.class.getName(), constpool);
            arrayMemberValue = new ArrayMemberValue(constpool);
            arrayMemberValue.setValue(getProduces(method));
            annotation.addMemberValue("value", arrayMemberValue);
            attribute.addAnnotation(annotation);

            annotation = new javassist.bytecode.annotation.Annotation(method.getVerb().getAnnotation().getName(),
                    constpool);
            attribute.addAnnotation(annotation);

            createdMethod.getMethodInfo().addAttribute(attribute);

            restClass.addMethod(createdMethod);
        } catch (CannotCompileException e) {
            throw new PutRpcToRestError("Error creatin method for service class. " + e, e);
        }

    }

    private String getJvmSignature(Method method) {
        StringBuffer jvmSignature = new StringBuffer();

        jvmSignature.append("(");
        for (Parameter parameter : method.getParameters()) {
            if (parameter.getCollectionType() == null) {
                jvmSignature.append(Descriptor.of(getCtClassForClass(parameter.getJavaType())));
            } else {
                jvmSignature.append(Descriptor.of(getCtClassForClass(parameter.getCollectionType()
                        .getBaseCollecionType())));
                jvmSignature.insert(jvmSignature.length() - 1,
                        "<" + Descriptor.of(getCtClassForClass(parameter.getJavaType())) + ">");
            }
        }
        jvmSignature.append(")");
        jvmSignature.append(Descriptor.of(getCtClassForClass(Object.class)));
        return jvmSignature.toString();
    }

    private void addManualBoxing(StringBuffer body, Parameter parameter, int i) {
        Class<?> clazz;
        if (parameter.getCollectionType() == null) {
            clazz = parameter.getJavaType();
        } else {
            clazz = parameter.getCollectionType().getBaseCollecionType();
        }
        if (clazz.isPrimitive()) {
            if (clazz.equals(int.class)) {
                body.append(" Integer.valueOf(");
            } else if (clazz.equals(char.class)) {
                body.append(" Character.valueOf(");
            } else if (clazz.equals(byte.class)) {
                body.append(" Byte.valueOf(");
            } else if (clazz.equals(short.class)) {
                body.append(" Short.valueOf(");
            } else if (clazz.equals(long.class)) {
                body.append(" Long.valueOf(");
            } else if (clazz.equals(float.class)) {
                body.append(" Float.valueOf(");
            } else if (clazz.equals(int.class)) {
                body.append(" Integer.valueOf(");
            } else if (clazz.equals(boolean.class)) {
                body.append(" Boolean.valueOf(");
            }

            body.append("$");
            body.append(i);
            body.append(")");
        } else {
            body.append("$");
            body.append(i);
        }
    }

    private MemberValue[] getConsumes(Method method) {
        return getMemberValueArrayFromCollection(method.getConsumes());
    }

    private MemberValue[] getProduces(Method method) {
        return getMemberValueArrayFromCollection(method.getProduces());
    }

    private MemberValue[] getMemberValueArrayFromCollection(Collection<String> list) {
        MemberValue[] consumes = new StringMemberValue[list.size()];

        int i = 0;
        for (String consumesValue : list) {
            consumes[i] = new StringMemberValue(consumesValue, constpool);
            i++;
        }
        return consumes;
    }

    private String getMethodName(Resource resource, Method method) {
        String firstProposalForMethodName = getJavaIdentifierForName("service_method_" + resource.getId() + "_"
                + method.getId());
        StringBuffer proposedMethodName = new StringBuffer(firstProposalForMethodName);
        while (methodNames.contains(firstProposalForMethodName)) {
            proposedMethodName.append('_');
        }

        return proposedMethodName.toString();
    }

    private CtClass[] getMethodSignature(Resource resource, Method method) {
        CtClass[] paramTypes = new CtClass[method.getParameters().size()];

        int i = 0;
        for (Parameter parameter : method.getParameters()) {
            if (parameter.getCollectionType() == null) {
                paramTypes[i] = getCtClassForClass(parameter.getJavaType());
            } else {
                paramTypes[i] = getCtClassForClass(parameter.getCollectionType().getBaseCollecionType());

            }
            i++;
        }
        return paramTypes;
    }

    private CtClass getCtClassForClass(Class<?> clazz) {
        try {
            return pool.getCtClass(clazz.getName());
        } catch (NotFoundException e) {
            throw new PutRpcToRestError("Erro creating service class! Could not find type " + clazz.getName());
        }

    }

    // @Override
    // public Class<?> generateClass(Services restServiceDefinitions) {
    // if (generatedClass == null) {
    // generatedClass = generateClassForModel(restServiceDefinitions);
    // }
    // return generatedClass;
    // }

    // public String getClassName(Services restServices) {
    //
    // String firstProposalForClassName = "org.putrpctorest.generatedclasses."
    // + getJavaIdentifierForName("RestServiceClass"
    // + restServices.getId());
    // StringBuffer proposedClassName = new StringBuffer(
    // firstProposalForClassName);
    //
    // while (true) {
    // try {
    // pool.get(proposedClassName.toString());
    // proposedClassName.append('_');
    // } catch (NotFoundException e) {
    // break;
    // }
    // }
    //
    // while (classAlreadyExists(proposedClassName.toString())) {
    // proposedClassName.append('_');
    // }
    // return proposedClassName.toString();
    // }

    private static String getJavaIdentifierForName(String name) {
        StringBuffer javaIdentifier = new StringBuffer();

        for (int i = 0; i < name.length(); i++) {
            if (Character.isJavaIdentifierPart(name.charAt(i))) {
                javaIdentifier.append(name.charAt(i));
            } else {
                javaIdentifier.append('_');
            }
        }

        return javaIdentifier.toString();

    }

    private static boolean classAlreadyExists(String className) {

        try {
            Class.forName(className);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

}
